<?php
/*
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.gnu.org/copyleft/lesser.html>.
 */

/**
 * @package Missus
 * @author  Denis Hovart <dhovart@gmail.com>
 * @license www.gnu.org/copyleft/lesser.html LGPL
 * @version 0.1
 */

class Missus {

    // =======
    // = Log =
    // =======
    /**
     * @var boolean
     */
    private $log;
    /**
     * @var boolean
     */
    private $log_html;

    // =================
    // = Configuration =
    // =================
    /**
     * @var boolean
     */
    protected $use_tls;
    /**
     * @var boolean
     */
    protected $use_bosh;
    /**
     * @var boolean
     */
    protected $timeout;

    // ====================
    // = Connection infos =
    // ====================
    /**
     * @var string
     */
    protected $host;
    /**
     * @var Integer
     */
    protected $port;
    /**
     * @var boolean
     */
    protected $is_bot;
    /**
     * @var string
     */
    protected $username;
    /**
     * @var string
     */
    protected $domain;
    /**
     * @var string
     */
    protected $password;

    // ==============
    // = XMPP infos =
    // ==============
    /**
     * @var string
     */
    protected $bare_jid;
    /**
     * @var string
     */
    protected $full_jid;
    /**
     * @var Integer
     */
    protected $id;
    /**
     * @var array
     */
    protected $roster;

    // ===================
    // = Events handling =
    // ===================
    /**
     * @var array
     */
    protected $events;
    /**
     * @var boolean
     */
    protected $authenticated;
    /**
     * @var boolean
     */
    protected $connected;
    /**
     * @var boolean
     */
    protected $tls_is_set;
    /**
     * @var boolean
     */
    protected $id_is_set;

    // ==========
    // = Stream =
    // ==========
    /**
     * @var string
     */
    protected $received;

    // =========
    // = Timer =
    // =========
    /**
     * @var integer
     */
     protected $time_start;

    // ======================================
    // = Missus main API and public methods =
    // ======================================
    /**
     * Constructor
     * @param string $username
     * @param string $password
     * @param string $domain
     * @param string $host If null, same as $domain
     * @param boolean $use_tls If set to true, will attempt a secured encypted connection over TLS (if proposed by server)
     * @param boolean $use_bosh (Not yet implemented)
     * @param Integer $port
     * @return Missus
     */
	public function __construct($username, $password, $domain, $host = null, $use_tls = true, $use_bosh = false, $port = 5222) {
		if(null === $username || null === $password || null === $domain)
		    throw new Exception("Username, password and host need to be specified");
		if(null === $host) $this->host = $domain;
		else $this->host = $host;
		$this->port = $port;
		$this->username	 = $username;
		$this->domain = $domain;
		$this->password = $password;
		$this->resource = "Missus";
		$this->full_jid = sprintf("%s@%s/%s", $this->username, $this->host, $this->resource);
		$this->bare_jid = sprintf("%s@%s", $this->username, $this->host);
		$this->received = '';
		$this->timeout = 20;
		$this->use_tls = $use_tls;
		$this->use_bosh = $use_bosh;
		$this->tls_is_set = false;
		$this->authenticated = false;
		$this->connected = false;
		$this->id_is_set = false;
		$this->events = array();
		$this->log = false;
	    $this->lang = 'en';
	    $this->is_bot = false;
    	$this->roster= array();
        if($this->use_bosh) throw new Exception("Bosh is not supported yet");
        $this->time_start = array_sum(explode(' ', microtime()));
	}

    /**
     * Performed when connecting
     */
    protected function onConnect() {}
    /**
     * Performed while Missus is running
     */
    protected function mainLoop() {}
    /**
     * Performed when receiving something new
     */
    protected function onReceivedContent() {}
    /**
     * Performed when a contact is composing a message 
     * @param string $from the user composing
     */
    protected function onUserComposing($from) {}
    /**
     * Performed when a contact paused the composition of his message 
     * @param string $from the user composing
     */
    protected function onUserPaused($from) {} # todo
    /**
     * Performed when a message is received 
     * @param string $body the contents of the message
     * @param string $from the user sending the message
     * @param string $type the type of message
     * @param string $xml the raw xml of the message
     */
    protected function onReceivedMessage($body, $from, $type, $xml) {}
    /**
     * Performed when a contact send a presence stanza
     * @param string $from the user
     * @param string $status the user status
     * @param string $availability the user availability
     */
    protected function onPresence($from, $status, $availability) {}
    /**
     * Performed when a contact send a subscription request
     * @param string $from the user
     */
    protected function onSubscribe($from) {}
    /**
     * Performed when a contact unsubscribe from the user
     * @param string $from the user
     */
    protected function onUnsubscribe($from) {}


    /**
     * Activate events log
     * @return Missus
     */
    public function logEvents() {
		$this->log = true;
		return $this;
    }
    /**
     * Set client lang
     * @param string $lang The lang you wish to set your content to
     * @link http://www.w3.org/TR/xml/#sec-lang-tag
     * @return Missus
     */
	public function setLang($lang) {
	    $this->lang = $lang;
	    return $this;
    }

    /**
     * Set the connection as persistent
     * @return Missus
     */
    public function setPersistent() {
        $this->is_bot = true;
	    return $this;
    }

    /**
     * Open a new stream to the specified host on the specified port, then attempt authentication
     */
	public function connect() {
		$socket = sprintf('tcp://%s:%d/', $this->host, $this->port);
        if($this->log) Logger::log(sprintf('Connecting to %s...', $socket), $this->log_html);
		$stream = @stream_socket_client($socket, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT);
		if(!$stream) throw new Exception(sprintf('Unable to open stream. Error %d: %s', $errno, $errstr));
		stream_set_blocking($stream, 0);
		$this->stream = $stream;
		$this->initiateXMLStream();
		$this->addEventListener('checkFeatures');
		$this->connected = true;
		$this->onConnect();
        if($this->is_bot) $this->addEventListener('checkStreamContent');
        $this->disconnect();
	}

    /**
     * Close XML stream and shut down socket
     */
	public function disconnect() {
	    $this->writeToStream('</stream:stream>');
		stream_socket_shutdown($this->stream, STREAM_SHUT_RDWR);
    }

    /**
     * Defines user presence
     * @param string $type
     * @param string $to
     * @param string $from
     * @param string $availability
     * @param string $status
     * @param string $priority
     * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.5.5
     * @return Missus
     */
    public function sendPresence($type = null, $to = null, $from = null, $availability = null, $status = null, $priority = null) {
        $presence = new SimpleXMLElement('<presence/>');
        if(null !== $type)  $presence->addAttribute('type', $type);
        if(null !== $to)    $presence->addAttribute('to', $to);
        if(null !== $from)  $presence->addAttribute('from', $from);
        if(null !== $availability) $presence->addChild('show', $availability);
        if(null !== $status)       $presence->addChild('status', $status);
        if(null !== $priority)     $presence->addChild('priority', $priority);
        $this->writeToStream($this->as_xml($presence));
        unset($presence);
        return $this;
    }

    /**
     * Set user availibility
     * @param string $availability 
     * @link http://xmpp.org/rfcs/rfc3921.html#stanzas-presence-children-show
     * @uses sendPresence
     * @return Missus
     */
    public function setAvailability($availability) {
        $this->sendPresence(null, null, null, $availability);
        return $this;
    }

    /**
     * Set user status
     * @param string $status 
     * @link http://xmpp.org/rfcs/rfc3921.html#stanzas-presence-children-status
     * @uses sendPresence
     * @return Missus
     */
    public function setStatus($status) {
        $this->sendPresence(null, null, null, null, $status);
        return $this;
    }

    /**
     * Set user priority
     * @param integer $priority 
     * @link http://xmpp.org/rfcs/rfc3921.html#stanzas-presence-children-priority
     * @uses sendPresence
     * @return Missus
     */
    public function setPriority($priority) {
        $this->sendPresence(null, null, null, null, null, $priority);
        return $this;
    }

    /**
     * Send a subscription request to a user
     * @param string $to the jid of the user you wish to subscribe to
     * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.6.1
     * @uses sendPresence
     * @return Missus
     */
    public function subscribe($to, $name = null, $group = null) {
        $this->addToRoster($to);
        $this->sendPresence('subscribe', $to);
        $this->askRoster();
        return $this;
    }

    /**
     * Subscribe back to a user
     * @param string $to the jid of the user you wish to subscribe back to
     * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.8.3
     * @uses subscribe
     * @uses sendPresence
     * @return Missus
     */
    public function subscribeBack($to, $name = null, $group = null) {
        $this->sendPresence('subscribed', $to);
        $this->subscribe($to);
        return $this;
    }

    /**
     * Decline subscription request
     * @param string $to
     * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.8.2.1
     * @uses sendPresence
     * @return Missus
     */
    public function declineSubscriptionRequest($from) {
        $this->sendPresence('unsubscribed', $from);
        $this->askRoster();
        return $this;
    }

    /**
     * Unsubscribe from a user
     * @param string $to the jid of the user you wish to unsubscribe from
     * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.6.1
     * @return Missus
     */
    public function unsubscribe($to) {
        $this->sendPresence('unsubscribe', $to);
        # TODO verify if the user is in the contact's roster before
        $this->removeFromRoster($to);
        $this->askRoster();
        return $this;
    }

    /**
     * Returns user roster
     * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.5.5
     * @return Array
     */
	public function getRoster() {
	    return $this->roster;
    }

    /**
     * Send a message to a user
     * @param string $to The jid of the user you wish to send a message to
     * @param string $body The content of your message 
     * @param string $subject If any, the subject of the message
     * @param string $thread If any, the thread the message belongs to
     * @param string $type The type of message. If null, set as 'chat'
     * @param string $payload Eventual XML payload
     * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.2.1
     */
	public function message($to, $body, $subject = null, $thread = null, $type = null, $payload = null) {
	    $type = (null === $type) ? 'normal' : $type;
		$this->writeToStream(sprintf(
		    '<message from="%s" to="%s" type="%s"><body>%s</body>%s</message>',
		    $this->full_jid, $to, $type, $body, $payload
		));
        return $this;
	}

    /**
     * Send an HTML message to a user
     * @uses message
     * @param string $contents The content of your message 
     * @param string $subject If any, the subject of the message
     * @param string $thread If any, the thread the message belongs to
     * @param string $type The type of message. If null, set as 'chat'
     * @link http://xmpp.org/extensions/xep-0071.html
     */
	public function messageHTML($to, $contents, $subject = null, $thread = null, $type = null) {
        $html = new SimpleXMLElement('<html/>', LIBXML_NOXMLDECL, false, 'http://jabber.org/protocol/xhtml-im');
        $html->addChild('body', $contents, 'http://www.w3.org/1999/xhtml');
	    $body = filter_var($contents, FILTER_SANITIZE_STRING); # strip HTML tags to include default message also
	    $this->message($to, $body, $subject, $thread, $type, $this->as_xml($html));
    }

    /**
     * Get a username (if any) by its jid
     */
	public function getUserByJID($jid) {} # TODO

    // ===================
    // = XML operations  =
    // ===================
    /**
     * Loads an xml string and turns it into a SimpleXMLElement object.
     * @return SimpleXMLElement|false
     */
    protected function load_xml_string($string, $wrap = false, $strip_ns = false) {
        if($wrap) $string = '<xml>' . $string . '</xml>';
        if($strip_ns) $string = $this->strip_ns($string);
        # we don't want to validate the (possibly broken) xml parsed (thus the use of ~LIBXML_DTDVALID)
        return @simplexml_load_string($string, 'SimpleXMLElement', ~LIBXML_DTDVALID);
    }
    /**
     * Output a simpleXMLObject as an xml string
     * I had to create my own method since the option LIBXML_NOXMLDECL wasn't ever implemented
     * http://bugs.php.net/bug.php?id=47137 | http://bugs.php.net/bug.php?id=50989
     * @return string
     */
    protected function as_xml($sxe) {
        return preg_replace('/^\<\?xml[^>]*>/', '', $sxe->asXML());
    }

    /**
     * Strip namespaces declarations; not very standard but it can cause some
     * problem for parsing with SimpleXML otherwise.
     */
    protected function strip_ns($string) {
        return preg_replace('/\s(xmlns=(?:"|\')[^"\']*(?:"|\'))/', '', $string);
    }

    // ==================
    // = Events methods =
    // ==================
    /**
     * Get elapsed time
     */
    protected function getElapsedTime() {
        $time = array_sum(explode(' ', microtime()));
        return round($time - $this->time_start, 5);
    }

    /**
     * Adds a "listener", that will call a custom-defined function that checks if an event occured.
     * You can then trigger whatever function you want as a callback in the function you just defined.
     * @param string $eventName The name of the event and of the function (must be of protected access) that you want to call
     */
    protected function addEventListener($eventName) {
        if($this->log) Logger::log('Adding event listener : ' . $eventName, $this->log_html);
        array_unshift($this->events, $eventName);
		$this->readFromStream();
    }

    /**
     * Remove last event
     */
    protected function removeEventListener() {
        if($this->log) Logger::log('Removing event listener : ' . $this->events[0], $this->log_html);
		array_shift($this->events);
    }
	
    /**
     * Execute the listeners in the events stack
     */
    private function listen() {
        if(preg_match("#^[\s]*$#", $this->received)) return;
        foreach($this->events as $event) call_user_func(array($this, $event));
    }
    
	// ==============================
	// = Stream read/write methods  =
	// ==============================
    /**
     * Write to the stream
     * @param string $contents
     */
    protected function writeToStream($contents) {
        if($this->log) Logger::log('Sending : ' . $contents, $this->log_html);
		stream_socket_sendto($this->stream, $contents);
    }

    /**
     * Read from the stream
     * @param integer $buffer The length of the max amount of bytes you want to read from the stream
     */
    protected function readFromStream($buffer = 8192) {
        $this->emptyReceivedContent();
        if($this->log) Logger::log('Waiting for new content', $this->log_html);
        $write = $except = null;
        while(count($this->events)) {
            $read = array($this->stream);
    		$updated_streams = stream_select($read, $write, $except, $this->is_bot ? 0 : $this->timeout);
            if($updated_streams === 0 && !$this->is_bot) throw new Exception('Timeout - server did not answer');
            $this->received .= stream_socket_recvfrom($this->stream, $buffer);
            if($this->connected) $this->mainLoop();
            $this->listen();
            usleep(0020000);
    	}
    	return true;
    }

    /**
     * Clean "buffer"
     */
    protected function emptyReceivedContent() {
        $this->received = '';
    }

    // ================
    // = XMPP Methods =
    // ================
    /**
     * Sends a new XML stream to server
     * @link http://tools.ietf.org/html/rfc3920#section-4
     */
    protected function initiateXMLStream() {
        $this->writeToStream(sprintf(
            "<?xml version='1.0'?><stream:stream xmlns='jabber:client' ".
            "xmlns:stream='http://etherx.jabber.org/streams' to='%s' xml:lang='%s' version='1.0'>",
            $this->domain, $this->lang
        ));
    }

    /**
     * Handle <stream:features/> tag
     * @link http://tools.ietf.org/html/rfc3920#section-4.6
     */
    private function checkFeatures() {
        $xml = $this->load_xml_string($this->received . '</stream:stream>');
        if(!$xml) return;
        $xml->registerXPathNamespace('stream', 'http://etherx.jabber.org/streams');
        if($features_query = $xml->xpath('/stream:stream/stream:features')) {
            if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
            $this->removeEventListener();
            $features = $features_query[0];
            if(count($features->bind) && $this->authenticated) $this->bindResource();
            else if(count($features->starttls) && $this->use_tls && !$this->tls_is_set) $this->askTLSConnection();
            else if(count($features->mechanisms)) {
                $mechs = array();
                foreach($features->mechanisms[0] as $mech) $mechs[] = strval($mech);
                $this->handleMechs($mechs);
            }
            else throw new Exception('No authentication mechanisms found');
        }
        unset($xml);
    }
    
    /**
     * Handle authentication mechanisms defined in stream features
     * Currently supported : PLAIN, DIGEST-MD5, ANONYMOUS
     * @link http://tools.ietf.org/html/rfc3920#section-5.3
     */
    private function handleMechs($mechs) {
        if(empty($mechs)) throw new Exception('No authentication mechanisms found');
        if($this->log) Logger::log('Authentification mechanisms are : ' . implode('; ', $mechs), $this->log_html);
        if(in_array('DIGEST-MD5', $mechs)) $this->digestAuth();
        else if(in_array('PLAIN', $mechs)) $this->plainTextAuth();
        #else if(in_array('ANONYMOUS', $mechs)) $this->anonymousAuth(); # TODO
    }

    /**
     * Ask for TLS connection
     * @link http://tools.ietf.org/html/rfc3920#section-5.1
     */
    private function askTLSConnection() {
        if($this->log) Logger::log('Asking for TLS connection...', $this->log_html);
        $this->writeToStream("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required /></starttls>");
		$this->addEventListener('checkTLSNegotiation');
	}

    /**
     * Custom "listener" to check if server allow us to proceed to stream encryption
     * @link http://tools.ietf.org/html/rfc3920#section-5.1
     */
    private function checkTLSNegotiation() {
        $xml = $this->load_xml_string($this->received);
        switch($xml->getName()) {
            case 'proceed' :
                if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
                $this->removeEventListener();
                if($this->log) Logger::log('Proceeding to TLS encrypted connection', $this->log_html);
                $this->startTLSConnection();
                break;

            default:
            case 'failure' :
                if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
                $this->removeEventListener();
                throw new Exception('Unable to connect over TLS');
                break;
        }
        unset($xml);
    }

    /**
     * Starts stream encryption
     * @link http://tools.ietf.org/html/rfc3920#section-5.1
     */
    private function startTLSConnection() {
    	stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
    	$this->tls_is_set = true;
        $this->initiateXMLStream();
		$this->addEventListener('checkFeatures');
    }

    /**
     * Ask for digest-md5 authentication
     */
    private function digestAuth() {
        if($this->log) Logger::log('Attempting DIGEST-MD5 authentication... ', $this->log_html);
		$this->writeToStream('<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="DIGEST-MD5"/>');
		$this->addEventListener('checkChallenge');
    }

    /**
     * Check if we received a challenge from the server (second part of the digest-md5 authentication routine)
     */
    private function checkChallenge() {
        if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
        $xml = $this->load_xml_string($this->received);
		$this->removeEventListener();
		$this->processChallenge(base64_decode(strval($xml[0])));
    }

    /**
     * Process the challenge received and send a response to the server (third part of the digest-md5 authentication routine)
     * @param $challenge
     */
    private function processChallenge($challenge) {
        if($this->log) Logger::log('Processing received challenge...', $this->log_html);
        $challenge_contents = explode(',', $challenge);
        $challenge_assoc = array();
        foreach($challenge_contents as $content) {
            preg_match('#(?P<key>.+)=(?P<value>.+)#', $content, $matches);
            $challenge_assoc[$matches['key']] = str_replace('"', '', $matches['value']);
        }
        $cnonce = md5(rand().time());
        $digest_uri = sprintf('xmpp/%s', $this->host);
        $realm = isset($challenge_assoc['realm']) ? $challenge_assoc['realm'] : '';
        $response = $this->generateDigestResponse(
            $this->username,
            $this->password,
            $realm,
            $challenge_assoc['nonce'],
            $cnonce,
            $digest_uri,
            $this->domain == 'chat.facebook.com' ? false : $this->full_jid);

        if(!isset($challenge_assoc['realm'])) $response_content = sprintf(
            'username="%s",nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,charset=utf-8',
            $this->username, $challenge_assoc['nonce'], $cnonce, $digest_uri, $response, $this->full_jid
        );
        else $response_content = sprintf(
            'username="%s",realm="%s",nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,charset=utf-8',
            $this->username, $challenge_assoc['realm'], $challenge_assoc['nonce'], $cnonce, $digest_uri, $response, $this->full_jid
        );
        if($this->domain != 'chat.facebook.com') $response_content .= sprintf(',authzid="%s"', $this->full_jid);

        $this->writeToStream(sprintf(
            '<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">%s</response>',
            base64_encode($response_content)
        ));
	    $this->addEventListener('checkChallenge2');
    }

    /**
     * Helper method that generates the 'response' part of our answer to the server
     */
    private function generateDigestResponse($username, $pass, $realm, $nonce, $cnonce, $digest_uri, $authzid = false) {
        if($authzid) $A1 = sprintf('%s:%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $username, $realm, $pass))), $nonce, $cnonce, $authzid);
        else $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $username, $realm, $pass))), $nonce, $cnonce);
        $A2 = 'AUTHENTICATE:' . $digest_uri;
        return md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($A1), $nonce, $cnonce, md5($A2)));
    }

    /**
     * Check if we received a second challenge from the server then wait for authentication confirmation
     */
    private function checkChallenge2() {
        if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
        $xml = $this->load_xml_string($this->received);
		$this->removeEventListener();
		if($xml[0]->getName() === 'failure') throw new Exception('Authentication failed');
        $this->writeToStream('<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></response>');
	    $this->addEventListener('checkAuthConfirmation');
    }

    /**
     * Send authentication credentials as plain-text then wait for authentication confirmation (fourth part of the digest-md5 authentication routine)
     */
    private function plainTextAuth() {
        if($this->log) Logger::log('Attempting plain-text authentication... ', $this->log_html);
		$this->writeToStream(sprintf(
		    "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>",
		    base64_encode(chr(0) . $this->username . chr(0) . $this->password)
		));
		$this->addEventListener('checkAuthConfirmation');
    }

    /**
     * Check if the server successfully authenticated the user, whatever the mechanism used was
     */
    private function checkAuthConfirmation() {
        if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
        $xml = $this->load_xml_string($this->received);
        switch($xml[0]->getName()) {
            default:
            case 'failure':
                $children = $xml[0]->children();
                throw new Exception('Unable to authenticate : ' . $children[0]->getName());
            break;
            case 'success':
                if($this->log) Logger::log('Successfully authenticated ', $this->log_html);
                $this->authenticated = true;
                $this->removeEventListener();
                $this->initiateXMLStream();
        		$this->addEventListener('checkFeatures');
            break;
        }
        unset($xml);
    }
    
    /**
     * Helper method that generates a unique id for the user and register it in the class attributes
     */
    private function generateId() {
        $this->id = uniqid();
        $this->id_is_set = true;
    }

    /**
     * Ask for binding resource id on the server
     * @link http://tools.ietf.org/html/rfc3920#section-7
     */
    private function bindResource() {
        $this->generateId();
        if($this->log) Logger::log('Attempting to bind resource on server...', $this->log_html);
        $this->writeToStream(sprintf(
            "<iq type='set' id='%s'>".
            "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>".
            "<resource>%s</resource>" .
            "</bind>" .
            "</iq>",
            $this->id,
            $this->resource
        ));
		$this->addEventListener('checkBoundResourceResult');
    }

    /**
     * Check if resource identifier was successfully bound on the server
     * @link http://tools.ietf.org/html/rfc3920#section-7
     */
    private function checkBoundResourceResult() {
        if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
        $xml = $this->load_xml_string($this->received);
        $iq_type_query = $xml->xpath('@type');
        switch($iq_type_query[0]) {
            case 'result':
                if($this->log) Logger::log('Resource identifier successfully bound on server', $this->log_html);
        		$this->removeEventListener();
                $this->full_jid = strval($xml[0]->bind->jid);
                $this->startSession();
                break;
            default:
            case 'error':
                $error_query = $xml[0]->error->children();
                $error = $error_query[0];
                switch($error->getName()) {
                    case 'bad-request':
                        throw new Exception('Resource identifier cannot be processed');
                        break;
                    case 'conflict':
                        throw new Exception('Resource identifier id already in use');
                        break;
                    case 'not-allowed':
                        throw new Exception('Not allowed to bind a resource');
                        break;
                }
                break;
        }
        unset($xml);
    }

    /**
     * Ask for a new session, then wait confirmation from server
     * @link http://xmpp.org/rfcs/rfc3921.html#session
     */
    private function startSession() {
    	$this->generateId();
        $this->writeToStream(sprintf(
            "<iq xmlns='jabber:client' type='set' id='%s'>" .
            "<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />" .
            "</iq>",
            $this->id
        ));
        $this->addEventListener('checkSessionStarted');
    }

    /**
     * Check if session started
     * @link http://xmpp.org/rfcs/rfc3921.html#session
     */
    private function checkSessionStarted() {
        $xml = $this->load_xml_string($this->received);
        if(!$xml) return;
        if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
        $result = strval($xml->attributes()->type);
        if($result == 'result') {
            if($this->log) Logger::log('Session started', $this->log_html);
            $this->removeEventListener();
            $this->askRoster();
        }
        else {
            # TODO : get error type
            throw new Exception('Unable to initiate session');
        }
    }

    /**
     * Ask for user roster
     * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
     */
    private function askRoster() {
		$this->writeToStream(sprintf(
			"<iq from='%s' type='get' id='roster_1'><query xmlns='jabber:iq:roster'/></iq>",
			$this->full_jid
		));
		$this->addEventListener('getRosterContents');
    }

    /**
     * Store user roster in an associative array
     * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
     */
	private function getRosterContents() {
        $xml = $this->load_xml_string($this->received, true, true);
        if(!$xml) return;
		if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
		$result = $xml->xpath("//iq[@type='result']/query");
		if(!$result) return;
		else $contacts = $result[0]->children();
		foreach($contacts as $contact) {
		    $attrs = $contact->attributes();
		    $this->roster[strval($attrs->jid)] = array(
		        'subscription' => strval($attrs->subscription),
		        'name' => isset($attrs->name) ? strval($attrs->name) : null
		    );
		}
		$this->removeEventListener();
    }
      
    /**
     * 
     * @link
     */
    private function addToRoster($jid, $name = null, $group = null) {
        $name = ($name !== null) ? $name : $jid;
        $group_str = '';
        if($group !== null) {
            if(is_array($group)) foreach($group as $g) $group_str .= '<group>' . $g . '</group>';
            else $group_str = $group;
        }
		$this->writeToStream(sprintf(
			"<iq from='%s' type='set' id='set1'><query xmlns='jabber:iq:roster'>" .
			"<item jid='%s' name='%s'>%s</item></query></iq>",
			$this->full_jid, $jid, $name, $group_str
		));
    }

    /**
     * 
     * @link
     */
    private function removeFromRoster($jid) {
		$this->writeToStream(sprintf(
            "<iq from='%s' id='roster_4' type='set'><query xmlns='jabber:iq:roster'><item subscription='remove' jid='%s'/></query></iq>",
			$this->full_jid, $jid
		));
    }

    // =================
    // = Bot behaviour =
    // =================
    /**
     * This function will be called in the main loop for persistent connections
     */
    private function checkStreamContent() {
#        Logger::log($this->received, false);
        $xml = $this->load_xml_string($this->received, true, true);
		$messages = $xml->xpath("//message");
		foreach($messages as $msg) {
		    $attrs = $msg->attributes();
		    $children = $msg->children();
		    $body = $children->body;
		    if(!empty($body)) {
		        $this->onReceivedMessage(strval($body), strval($attrs->from), strval($attrs->type), $msg->asXML());
		        continue;
		    }
		    if(isset($children->composing)) $this->onUserComposing(strval($attrs->from));
		    else if(isset($children->paused)) $this->onUserPaused(strval($attrs->from));
		}
		$presences = $xml->xpath("//presence");
		foreach($presences as $presence) {
		    $attrs = $presence->attributes();
		    $children = $presence->children();
		    $status = isset($children->status) ? strval($children->status) : null;
		    $availability = isset($children->show) ? strval($children->show) : null;
		    if($attrs->type == 'subscribe') $this->onSubscribe(strval($attrs->from));
		    else if($attrs->type == 'unsubscribe') $this->onUnsubscribe(strval($attrs->from));
		    else $this->onPresence(strval($attrs->from), $status, $availability);
	    }
		$this->onReceivedContent();
		$this->emptyReceivedContent();
    }
}

/**
 * A very simple logger.
 * @package Logger
 */
class Logger {

    static public function log($log, $html = false) {
        echo '__________'. ($html ? "\n<br />" : "\n");
        echo '['. strftime('%H:%M:%S') . '] ' . ($html ? htmlentities($log) : $log) . ($html ? "\n<br />" : "\n");
    }
}
?>